// Copyright (c) 2008 Matthew Johnson
// Licensed under the MIT License (see license.txt)
package actionml.display {
	import actionml.Global;
	import actionml.core.LoadingUtils;
	import actionml.animation.ExpressionRenderer;
	import actionml.animation.RenderManager;
	
	import flash.display.Bitmap;	
	import flash.display.BitmapData;
	import flash.display.DisplayObject;
	import flash.display.DisplayObjectContainer;
	import flash.display.Graphics;
	import flash.display.MovieClip;
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.external.ExternalInterface;
	import flash.geom.Point;
	import flash.geom.Rectangle;
    import flash.net.navigateToURL;
	import flash.net.URLRequest;
	import flash.net.URLRequestHeader;
	import flash.net.URLVariables;
	import flash.text.TextField;
	import flash.utils.ByteArray;
		
	public class DisplayObjectMethods {
		public static function initialize() {
			Global.initializeExtension(DisplayObjectMethods, DisplayObject);
		}
		public static var instanceMethods:Object = {
			loadCaptions: function() {
				var o:DisplayObject = this;
				LoadingUtils.requestURL(this["captionFile"], {
					completeHandler: function(captions:Object) {
						o.captions = [];
						for each (var caption:XML in XML(captions).elements()[1].elements()[0].elements()) {
							var text:Array = [];
							for each (var textXml:XML in caption.text()) {
								text.push(textXml.toString());
							}
							o.captions.push({
								begin: convertSmpte(caption.attribute("begin")),
								end: convertSmpte(caption.attribute("end")),
								text: text.join("\n")
							});
							function convertSmpte(time:String) {
								var match:Object = time.match(/(?P<hours>\d):(?P<minutes>\d\d):(?P<seconds>\d\d).\d+/);
								if (match && !o.isA("Video")) {
									return 360 * Number(match.hours) + 60 * Number(match.minutes) + Number(match.seconds) * o.stage.frameRate; // convert seconds to frames when controlling a movie clip instead of a video
								} else if (match) {
									return 360 * Number(match.hours) + 60 * Number(match.minutes) + Number(match.seconds);
								} else {
									return Number(time);
								}
							}
						}
						o.captions.sort(function(first:Object, second:Object) {
							if (first.begin == second.begin) {
								return 0;
							} else if (first.begin < second.begin) {
								return 1;
							} else {
								return -1;
							}
						});
					}
				});	
			},
			saveToImage: function() {
				this.doEvent("beginSaveToImage");
				var r:Rectangle = this.getBounds(this);
				// the children need to be shifted so they are 
				// aligned to 0, 0 in order for the bd.draw method
				// to work correctly
				if (this is DisplayObjectContainer) {
					for (var i:int = 0; i < this.numChildren; i++) {
						var child:DisplayObject = this.getChildAt(i);
						child.x -= r.x;
						child.y -= r.y;
					}
				}
				var bd:BitmapData = new BitmapData(this.width, this.height);
				bd.draw(this);
				// put the children back to where they were
				if (this is DisplayObjectContainer) {
					for (i = 0; i < this.numChildren; i++) {
						child = this.getChildAt(i);
						child.x += r.x;
						child.y += r.y;
					}
				}
				var blockSize:int = 50;
				var numBlocks:int = 1 + Math.floor(this.height / blockSize);
				var completedBlocks:int = 0;
				var currentBlock:int = 1;
				var frameCounter:int = 1;
				var obj:DisplayObject = this;
				this.addEventListener(Event.ENTER_FRAME, function() {
					frameCounter++;
					// let flash render a few frames before rendering a block
					// also make sure the first block completes before processing
					// additional blocks so the session is alive when subsequent
					// requests are sent
					if (currentBlock * 5 != frameCounter || currentBlock > 1 && completedBlocks < 1) return;

					// the server script expects a comma separated list of 
					// pixels for each row in the image to be sent as
					// post variables					
					var variables:URLVariables = new URLVariables();
					var firstLine:int = (currentBlock - 1) * blockSize;
					var lastLine:int = currentBlock * blockSize - 1;
					if (lastLine > obj.height - 1) lastLine = obj.height - 1;
					var lastPixelOfLine:int = obj.width - 1;
					for (i = firstLine; i <= lastLine; i++) {
						var pixel:String = "px" + i;
						variables[pixel] = "";
						for (var j:int = 0; j < obj.width; j++) {
							var pixelData:String = bd.getPixel(j, i).toString(16); 
							if (j < lastPixelOfLine) {
								pixelData += ",";
							}
							variables[pixel] += pixelData;
						}
					}
					variables.width = obj.width;
					variables.height = obj.height;
					variables.block = currentBlock;
					variables.block_size = blockSize;
					var url:URLRequest = new URLRequest("http://www.capella-id.com/media_scripts/jpg_maker/classroom.jpg");
					url.data = variables;
					url.method = "POST";
					url.contentType = "multipart/form-data";
					url.requestHeaders.push( new URLRequestHeader( 'Cache-Control', 'no-cache' ) );
					// the data is sent to the server with requestURL 
					// once the server has the data navigateToURL is used
					// to force the web browser to download the image.
					// the two phase protocol is used because this is the only
					// way to know when the post upload of the data has completed
					// before telling the browser to download the image
					LoadingUtils.requestURL("http://www.capella-id.com/media_scripts/jpg_maker/" + (obj.savedImageName || "export") + ".jpg", {
						data: variables,
						completeHandler: function(o:Object) {
							if (completedBlocks == 0) {
								// reset frame counter after first block completes 
								// to allow rest of blocks to process now that the
								// session is alive
								frameCounter = 1;
							}
							completedBlocks++;
							if (completedBlocks == numBlocks) {
								url = new URLRequest("http://www.capella-id.com/media_scripts/jpg_maker/" + (obj.savedImageName || "export") + ".jpg");
								url.data = new URLVariables();
								url.data.display = "true";
								url.method = "POST"
								navigateToURL(url, "_self"); 
								obj.doEvent("completeSaveToImage");
							}
						},
						statusHandler: function(e:Object) {
							trace("post image status")
							trace(e)	
						},
						errorHandler: function(e:Object) {
							// TODO: trace error message
							trace("post image load error")
							trace(e)	
							completedBlocks++;
							trace(completedBlocks)
							if (completedBlocks == numBlocks) {
								obj.doEvent("completeSaveToImage");
							}
						}
					});
					currentBlock++;
					if (currentBlock > numBlocks) {
						bd.dispose();
						obj.removeEventListener(Event.ENTER_FRAME, arguments.callee);
					}
				});
			},
			playOnce: function() {
				this.gotoAndStop(0);
				this.playToEnd();
			},
			playToEnd: function() {
				var o:DisplayObject = this;
				this.playing = true;				
				o.addEventListener(Event.ENTER_FRAME, function() {
					if (o.currentFrame == o.totalFrames) {
						o.stop();
						o.removeEventListener(Event.ENTER_FRAME, arguments.callee);
						o.doEvent("completeTimeline");
					}
				});
				o.play();
			},
			followLink: function() {
				var t:Object = DisplayObjectMethods["getTracer"]({method:"followLink", context: "ivars"})
				if (!("href" in this)) {
					t.error("followLink was called on an object with no href. An href must supply the URL for the link to follow.")
					return;
				}
	            var request:URLRequest = new URLRequest(this.href);
	            try {            
	                navigateToURL(request);
	            } catch (e:Error) {
   					t.error("An error occurred while following the link: '" + this.href + "'.  The error was:\n\t" + e);
				}	
			},
			show: function() {
				this.visible = true;
			},
			hide: function() {
				this.visible = false;
			},
			getLayer: function() {
				for (var i:int = 0; i < this.parent.numChildren; i++) {
					if (this.parent.getChildAt(i) == this) {
						return i + 1;
					}
				}
			},
			setLayer: function(layer:int) {
				this.parent.setChildIndex(this, layer - 1);
			},
			moveToFront: function() {
				this.parent.setChildIndex(this, this.parent.numChildren - 1);
			},
			moveForward: function() {
				var i:int = this.parent.getChildIndex(this);
				if (i < this.parent.numChildren - 1) {
					this.parent.setChildIndex(this, i + 1);
				}
			},
			moveToBack: function() {
				this.parent.setChildIndex(this, 0);
			},
			moveBackward: function() {
				var i:int = this.parent.getChildIndex(this);
				if (i > 0) {
					this.parent.setChildIndex(this, i - 1);
				}
			},
			removeFilters: function() {
				this.filters = [];
			},
			remove: function() {
				this.parent.removeChild(this);
			},
		
			getMousePosition: function() {
				return new Point(this.mouseX, this.mouseY);
			},
			getGlobalMousePosition: function() {
				return this.localToGlobal(new Point(this.mouseX, this.mouseY));
			},
			getGlobalMouseX: function() {
				return this.localToGlobal(new Point(this.mouseX, this.mouseY).x);
			},
			getGlobalMouseY: function() {
				return this.localToGlobal(new Point(this.mouseX, this.mouseY).y);
			},

			getKeypath: function(separator:String = ".") {
				if (this == Global.document) {
					return "";
				} else {
					var parentKeypath:String = this.parent.getKeypath(separator);
					if (parentKeypath == "") {
						return this.name;
					} else {
						return parentKeypath + separator + this.name;
					}
				}
			},
			getDepthLevel: function() {
				var depthLevel:Array = [this.parent.getChildIndex(this)];
				var ancestor:DisplayObjectContainer = this.parent;
				while (ancestor) {
					if (ancestor == ancestor.stage) {
						return depthLevel;
					} else {
						depthLevel.unshift(ancestor.parent.getChildIndex(ancestor));
					}
					ancestor = ancestor.parent;
				}
				return depthLevel;
			},

			hasChild: function(o:DisplayObject) {
				if (!("getChildIndex" in this)) {
					return false;
				}
				try {
					this.getChildIndex(o);
					return true;
				} catch (e:ArgumentError) {
					return false;
				}
			},
			hasAncestor: function(o:DisplayObject) {
				var parent:DisplayObject = this.parent;
				while (parent != this.stage) {
					if (parent == o) {
						return true;
					}
					parent = parent.parent;
				}
				return false;
			},
			getVisibleDescendents: function() {
				var o:DisplayObject = this;
				return this.filterTree(function(d:DisplayObject) {
					var p:DisplayObject = d;
					while (p != o) {
						if (!p.visible) {
							return false;
						}
						p = p.parent;
					}
					return true;
				});
			},
			getEdgeIntersectionAtAngle: function(angle:Number, margin:Number=0) {
				var p:Point = this.getBounds(this.parent).getEdgeIntersectionAtAngle(angle);
				if (this.hitTestPoint(p.x, p.y, true)) {
					do {
						// bring the point further from the center 
						p = p.addRadians(1, angle);
					} while (this.hitTestPoint(p.x, p.y, true))
					return p.addRadians(margin, angle);
				} else {
					do {
						// bring the point closer to the center
						p = p.addRadians(-1, angle);
					} while (!this.hitTestPoint(p.x, p.y, true))
					return p.addRadians(1 + margin, angle);
				}
			},	
			
			isThisOrChildTopObjectUnderPoint: function(o:*, y:* = null, shapeFlag = false, excluded:* = null) {
				var o:DisplayObject = Global.stage["getTopObjectUnderPoint"](o, y, shapeFlag, excluded);
				if (!o) {
					return false;
				}
				while (o != Global.stage) {
					if (this == o) {
						return true;
					}
					o = o.parent;
				}
				return false;
			},
			getTopObjectUnderPoint: function(o:*, y:* = null, shapeFlag = false, excluded:* = null) {
				var p:*;
				if (y != null) {
					p = new Point(o, y);
				} else {
					p = new Point(o.x, o.y);
				}
				if (!this.visible || this == excluded || (this.parent != null && this.parent == excluded)) {
					return null;
				}
				if (this is DisplayObjectContainer) {
					var topChild:DisplayObject;
					// descendents are on top of ancestors and 
					// top children are last in the child order
					// so we must iterate the display list backwards
					for (var i = this.numChildren - 1; i >= 0; i--) {
						topChild = this.getChildAt(i).getTopObjectUnderPoint(o, y, shapeFlag, excluded);
						if (topChild) {
							return topChild;
						}	
					}
				}
				if (this.hitTestPoint(p.x, p.y, shapeFlag)) {
					return this;
				}
				return null;
			},
			getTopMatchingObjectUnderPoint: function(selector:*, o:*, y:* = null, shapeFlag = false) {
				var p:*;
				if (y != null) {
					p = new Point(o, y);
				} else {
					p = new Point(o.x, o.y);
				}
				if (this is DisplayObjectContainer) {
					var topChild:DisplayObject;
					// descendents are on top of ancestors and 
					// top children are last in the child order
					// so we must iterate the display list backwards
					for (var i = this.numChildren - 1; i >= 0; i--) {
						// TODO: why is the child sometimes null???
						var child:DisplayObject =  this.getChildAt(i);
						if (child) { 
							topChild = child.getTopMatchingObjectUnderPoint(selector, o, y, shapeFlag);
							if (topChild) {
								return topChild;
							}
						}
					}
				}
				if (this.hitTestPoint(p.x, p.y, shapeFlag) && selector.isApplicable(this)) {
					return this;
				}
				return null;
			},

			hitTestRect: function(r:Rectangle) {
				return this.getBounds(this.stage).intersects(r);
			},
			hitTestVisibleDescendents: function(p:*, y:*=null, shapeFlag:Boolean=true) {
				if (y) {
					p = new Point(p, y);
				}
				for each (var d:DisplayObject in this.getVisibleDescendents()) {
					if (d.hitTestPoint(p.x, p.y, shapeFlag)) {
						return true;
					}
				}
				return false;
			},

			drawRect:  function(rect:Rectangle, thickness:Number = 1, color:Number = 0xFF0000 /*red*/, fillColor:int = -1, alpha:Number = 1.0) {
				var g:Graphics = this.graphics;
				g.clear();
				// it is important to clear the graphics before we get the bounds
				// if we do it the other way around our bounds will be larger each
				// time through as they will include the previous bounds
				if (fillColor >= 0) {
					g.beginFill(color, alpha);
				}
				g.lineStyle(thickness, color);
				g.moveTo(rect.left, rect.top);
				g.lineTo(rect.right, rect.top);
				g.lineTo(rect.right, rect.bottom);
				g.lineTo(rect.left, rect.bottom);
				g.lineTo(rect.left, rect.top);
				if (fillColor >= 0) {
					g.endFill
				}
			},
			
			addToShuffleGroup: function() {
				this.parent["shuffleGroup_" + this.shuffleGroup] ||= [];
				var g:Array = this.parent["shuffleGroup_" + this.shuffleGroup];
				g.push(this);
				var o:DisplayObject = this;
				this.addEventListener("init", function() {
					if (!g.shuffled) {
						var shuffled:Array = g["shuffle"]();
						for (var i:int = 0; i < g.length; i++) {
							g[i].x = shuffled[i]["symbol" + "X"];
							g[i].y = shuffled[i]["symbol" + "Y"];
						}
						g.shuffled = true;
					}
					o.removeEventListener("init", arguments.callee);
				});
			},
			
			findChild: function(name:String, options:Object=null) {
				if (options && name in options && options[name] != null && options[name] != undefined) {
					return this.getChildByName(options[name]);
				} else {
					if (this[name] is String) {
						return this.getChildByName(this[name]);
					} else {
						return this.getChildByName(name);
					}
				}
			},
			showChild: function(childName:String) {
				/// TODO: possibly allow for tweens here and in hideChild
				var o:DisplayObject = this.getChildByName(childName);
				if (o) {
					o.visible = true;
				}
			},
			hideChild: function(childName:String) {
				var o:DisplayObject = this.getChildByName(childName);
				if (o) {
					o.visible = false;
				}
			},
			doEvent: function(event:Object, options:Object=null) {
				options ||= {};
				if (this is EventDispatcher && !options["dontDispatch"]) {
					if (event is Event) {
						this.dispatchEvent(event);
					} else {
						this.dispatchEvent(new Event(event));
					}
				}
				if (event is Event) {
					event = event.type;
				}

				this.tryMethod(event + "Handler", this, event);
				options.tryMethod(event + "Handler", this, event);
				// TODO: is this the correct order of action???
				this.doInteractionsFor(event);
				this.tryMethod("on" + event.toCapitalCase() + "Handler", this, event)
				this.playTweensFor(event);
			},
			doInteractionsFor: function(event:String) {
				var eventName:String = "on" + event.toCapitalCase();
				if (eventName in this) {
					if (this[eventName] is Function) {
						this[eventName](this, event);
					} else {
						for each (var s:String in this[eventName].split(/\s*,\s*/)) {
							this.doAction(s, event);
						}
					}
				}
			},
			doAction: function(s:String, event:String) {
				var source:Object = this;
				var o:Object = this.getValueForKeypath(s.withoutLastPart());
				var method:String = s.lastPart();
				var match:Object;
				var property:String;
				var sourceProperty:String;
				if (method in o && o[method] is Function) {
					o.tryMethod(method, this, event);
				} else if ("actions" in Global && method in Global["actions"]){
					Global["actions"][method].call(o, this, event);
				} else if (method.match(/^take.+FromKeypath.+$/)){
					match = method.match(/^take(?P<property>.+)FromKeypath(?P<sourceProperty>.+)$/);
					property = match.property.toCamelCase();
					sourceProperty = match.sourceProperty.toCamelCase();
					o[method] = function() {
						o.setValueForKeypath(property, arguments[0].getValueForKeypath(arguments[0].getValueForKeypath(sourceProperty)));
					}
					o[method](this, event);
				} else if (method.match(/^take.+FromExpression.+$/)){
					match = method.match(/^take(?P<property>.+)FromKeypath(?P<sourceProperty>.+)$/);
					property = match.property.toCamelCase();
					sourceProperty = match.sourceProperty.toCamelCase();
					o[method] = function() {
						o.setValueForKeypath(property, arguments[0].getValueForKeypath(arguments[0].evaluateExpression(sourceProperty)));
					}
					o[method](this, event);
				} else if (method.match(/^take.+From.+$/)){
					match = method.match(/^take(?P<property>.+)From(?P<sourceProperty>.+)$/);
					property = match.property.toCamelCase();
					sourceProperty = match.sourceProperty.toCamelCase();
					o[method] = function() {
						o.setValueForKeypath(property, arguments[0].getValueForKeypath(sourceProperty));
					}
					o[method](this, event);
				} else if (method.match(/^take.+$/)){
					property = method.match(/^take(?P<property>.+)$/).property.toCamelCase();
					o[method] = function() {
						o.setValueForKeypath(property, arguments[0].getValueForKeypath(property));
					}
					o[method](this, event);
				} else if (method.match(/^append.+FromKeypath.+$/)){
					match = method.match(/^append(?P<property>.+)FromKeypath(?P<sourceProperty>.+)$/);
					property = match.property.toCamelCase();
					sourceProperty = match.sourceProperty.toCamelCase();
					o[method] = function() {
						o.setValueForKeypath(property, o.getValueForKeypath(property) + arguments[0].getValueForKeypath(arguments[0].getValueForKeypath(sourceProperty)));
					}
					o[method](this, event);
				} else if (method.match(/^append.+FromExpression.+$/)){
					match = method.match(/^append(?P<property>.+)FromKeypath(?P<sourceProperty>.+)$/);
					property = match.property.toCamelCase();
					sourceProperty = match.sourceProperty.toCamelCase();
					o[method] = function() {
						o.setValueForKeypath(property, o.getValueForKeypath(property) + arguments[0].getValueForKeypath(arguments[0].evaluateExpression(sourceProperty)));
					}
					o[method](this, event);
				} else if (method.match(/^append.+From.+$/)){
					match = method.match(/^append(?P<property>.+)From(?P<sourceProperty>.+)$/);
					property = match.property.toCamelCase();
					sourceProperty = match.sourceProperty.toCamelCase();
					o[method] = function() {
						o.setValueForKeypath(property, o.getValueForKeypath(property) + arguments[0].getValueForKeypath(sourceProperty));
					}
					o[method](this, event);
				} else if (method.match(/^append.+$/)){
					property = method.match(/^append(?P<property>.+)$/).property.toCamelCase();
					o[method] = function() {
						o.setValueForKeypath(property, o.getValueForKeypath(property) + arguments[0].getValueForKeypath(property));
					}
					o[method](this, event);
				} else if (method.match(/^traceExpression.+$/)){
					property = method.match(/^trace(?P<property>.+)$/).property.toCamelCase();
					o[method] = function() {
						// TODO: use a tracer
						trace(o.name + "." + property + ": " + o.evaluateExpression(property));
					}
					o[method](this, event);
				} else if (method.match(/^rotate.+$/)){
					var rotation:Number = Number(method.match(/^rotate(?P<rotation>.+)$/).rotation);
					o[method] = function() {
						o.rotation += rotation;
					}
					o[method](this, event);
				} else if (method.match(/^track.+$/)){
					var page:String = method.match(/^track(?P<page>.+)$/).page.toCamelCase();
					o[method] = function() {
						if (ExternalInterface.available) {
							ExternalInterface.call("dcsMultiTrack", "DCS.dcsuri", Global.swfServerRelativeURL.replace(".swf", "/") + o.getKeypath("/") + "/" + page + ".html", "WT.ti", o[page + "Title"]);
						} 
					}
					o[method](this, event);
				} else if (method.match(/^trace.+$/)){
					property = method.match(/^trace(?P<property>.+)$/).property.toCamelCase();
					o[method] = function() {
						// TODO: use a tracer
						trace(o.name + "." + property + ": " + o.getValueForKeypath(property));
					}
					o[method](this, event);
				} else if (method.match(/^do.+$/)){
					match = method.match(/^do(?P<event>.+)$/);
					o.doEvent(match.event.toCamelCase());
				} else if (method.match(/^goto.+$/)){
					var location:String = method.match(/^goto(?P<location>.+)$/).location.toCamelCase();
					if (isNaN(location)) {
						o[method] = function() {
							o.gotoAndStop(location);
						};
					} else {
						o[method] = function() {
							o.gotoAndStop(int(location));							
						}
					};
					o[method](this, event);
				} else if (method.match(/^playTo.+$/)){
					var frame:String = method.match(/^playTo(?P<location>.+)$/).location.toCamelCase();
					if (isNaN(frame)) {
						o[method] = function() {
							o.play();
							o.addEventListener(Event.ENTER_FRAME, function() {
								if (o.currentLabel == frame) {
									o.stop();
									o.removeEventListener(Event.ENTER_FRAME, arguments.callee);
								}
							});
						};
					} else {
						o[method] = function() {
							o.play();
							o.addEventListener(Event.ENTER_FRAME, function() {
								if (o.currentFrame == int(frame)) {
									o.stop();
									o.removeEventListener(Event.ENTER_FRAME, arguments.callee);
								}
							});							
						}
					};
					o[method](this, event);
				} else if (method.match(/^play.+$/)){
					var tween:String = method.match(/^play(?P<tween>.+)$/).tween.toCamelCase();
					o[method] = function() {
						o.playTween(tween);
					}
					o[method](this, event);
				} else {
					DisplayObjectMethods["getTracer"]({method:"doAction", context:"xml"}).error("'" + s + "' is not a valid action.");
				}
			},
			playTweensFor: function(event:String) {
				if (event + "Tweens" in this) {
					for each (var s:String in this[event + "Tweens"].split(/\s*,\s*/)) {
						var o:Object = this.getValueForKeypath(s.withoutLastPart());
						o.playTween(s.lastPart());
					}
				}
				if ("tweens" in this && this["tweens"]["on" + event.toCapitalCase()]) {
					this.playTween("on" + event.toCapitalCase());
				}
			},	
			configureFromXML: function(xml:XML, stop:Boolean = true):DisplayObject {
				// TODO: allow for XML to specify a mask??? (and possibly other special attributes)
				var t:Object = DisplayObjectMethods["getTracer"]({method:"configureFromXML", context:"xml"});
				if (stop) {
					this.walkTree(function(o:*) {
						if ("stop" in o && o.stop is Function) {
							try {
								o.stop();
							} catch (e:Error) { // if there is no video source we will get the video error
								if (!e["constructor"].toString().contains("VideoError")) {
									throw e;
								}
							}
						}
						if ("gotoAndStop" in o && o.gotoAndStop is Function) {
							o.gotoAndStop(1);
						}
					})
				}		
				for each (var a in ["x", "y", "width", "height", "scaleX", "scaleY", "rotation", "alpha"]) {
					try {
						this["symbol" + a.slice(0, 1).toUpperCase() + a.slice(1)] = this[a];
					} catch (e:ReferenceError){ /* swallow exception if we have a non-dynamic object */ }
				}
				var interactionMap:Object = {
					"shuffleGroup": "addToShuffleGroup",
					"bgImage": "loadBackgroundImage",
					"href": "followLinkOnClick",
					"onClick": "makeButton",
					"clickTweens": "makeButton",
					"onFocus": "triggerFocusInActions",
					"focusTweens": "triggerFocusInActions",
					"onBlur": "triggerFocusOutActions",
					"blurTweens": "triggerFocusOutActions",
					"inputMask": "maskInput",
					"captionFile": "loadCaptions",
					"require": "requireValues",
					"Image": "makeDynamicMedia",
					"Swf": "makeDynamicMedia" 
				};
				// TODO handle the fact that events can occur in actions, tweens, and assignments now
				var attributes:Array = Object(xml).getAttributes();
				delete attributes["symbol"];
				delete attributes["instanceName"];
				var setupFunctions:Array = [];
				var localName:String = Object(xml).localName();
				for each (var tag:String in Global.tags) {
					if (this[tag] is Function) {
						if (tag == localName || tag.replace(/^make/, "") == localName || localName["endsWith"](tag.replace(/^make/, "")) || localName["startsWith"](tag.replace(/^make/, ""))) {
							setupFunctions.push(tag);
						} 
						if (tag == localName || tag.replace(/^make/, "").toCamelCase() == localName || localName["startsWith"](tag.replace(/^make/, "").toCamelCase())) {
							setupFunctions.push(tag);
						}
					}
				}
				for (var im:String in interactionMap) {
					if (im == localName || localName["endsWith"](im) || localName["startsWith"](im)) {
						//setupFunctions.push(im);
						setupFunctions.push(interactionMap[im]);
					} 
				}
				if (Object(xml).localName() in this && this[Object(xml).localName()] is Function) {
					setupFunctions.push(Object(xml).localName());
				} else if ("make" + Object(xml).localName() in this && this["make" + Object(xml).localName()] is Function) {
					setupFunctions.push("make" + Object(xml).localName());
				} else if (interactionMap[Object(xml).localName()] in this && this[interactionMap[Object(xml).localName()]] is Function) {
					setupFunctions.push(interactionMap[Object(xml).localName()]);
				}
				var expressions:Array = [];
				for (var p in attributes) {
					if (p == "interactions") {
						for each (var f:String in attributes[p].split(/\s*,\s*/)) {
							if (f in this) {
								setupFunctions.push(f);
							} else if ("make" + f in this) {
								setupFunctions.push("make" + f);
							} else {
								t.error("'" + f + "' was not found on + '" + this + "'.  This interaction is specified in error by the xml which should be checked for errors:\n\t" + xml);							
							}
						}
					} else {
						if (p in this && this[p] is Function) {
							setupFunctions.push(p); 
						} else if (interactionMap[p] in this && this[interactionMap[p]] is Function) {
							setupFunctions.push(interactionMap[p]);
						}
						try {
							if (p in this && (this[p] != null)) {
								this["symbol" + p.slice(0, 1).toUpperCase() + p.slice(1)] = this[p];
							} else {
								this["symbol" + p.slice(0, 1).toUpperCase() + p.slice(1)] = attributes[p];
							}
						} catch (e:ReferenceError){ /* swallow exception if we have a non-dynamic object */ }
						try {
							this["xml" + p.slice(0, 1).toUpperCase() + p.slice(1)] = attributes[p];
						} catch (e:ReferenceError){ /* swallow exception if we have a non-dynamic object */ }
						try {
							if (attributes[p] is String && attributes[p].startsWith("{")) {
								RenderManager.addBinding(this, p, attributes[p]);
							} else if (attributes[p] is String && attributes[p].match(/^(\s*(\d*\.?\d*|\w+(\.\w+)*)\s*)([+\-\\*\/()]+(\s*(\d*\.?\d*|\w+(\.\w+)*)\s*))*$/) && this.getValueForKeypath(p) != null && !isNaN(Number(this.getValueForKeypath(p)))) {
								expressions.push({property: p, expression: attributes[p]});	
							} else if (attributes[p] is String && attributes[p].startsWith("(")) {
								expressions.push({property: p, expression: attributes[p]});
							} else {
								this.setValueForKeypath(p, attributes[p]);	
							}
						} catch (e:ReferenceError){ /* swallow exception if we have a non-dynamic object */ }
					}
				}
				for each (var exp:Object in expressions) {
					this.setValueForKeypath(exp.property, this.getValueForKeypath(exp.property.withoutLastPart()).evaluateExpression(exp.expression));
				}
				if (this.constructor.toString().contains("TextArea")) { // allows test without requiring reference & import of TextArea into all swfs
					this.makeTextAreaAccessible();
					this.htmlText = xml.toString().replace(new RegExp("^<" + Object(xml).localName() + ">"), "").replace(new RegExp("</" + Object(xml).localName() + ">$"), "");
				}
				if (this is TextField && Object(xml).children().length() > 0) {
					this.htmlText = xml.toString().replace(new RegExp("^<" + Object(xml).localName() + ">"), "").replace(new RegExp("</" + Object(xml).localName() + ">$"), "");
				}
				if (this is DisplayObjectContainer) {
					this.loadChildrenFromXML(xml, stop);
				}
				for each (var setupFunction:String in setupFunctions) {
					if (setupFunction is Function) {
						setupFunction();
					} else if (setupFunction in this) {
						if (this[setupFunction] is Function) {
							this[setupFunction](attributes);
						} else {
							t.error("'" + setupFunction + "' on + '" + this + "' is not a function.  This interaction is specified in error by the xml which should be checked for errors:\n\t" + xml);
						}
					} else {
						t.error("'" + setupFunction + "' was not found on + '" + this + "'.  This interaction is specified in error by the xml which should be checked for errors:\n\t" + xml);							
					}
				}				
				return this;
			}
		};
	}
}